﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Linq;
using System.Data.Linq.Mapping;
using System.Diagnostics;

namespace Close2Poco
{
    //TODO: add dbo. prefix to tables
    /// <summary>
    /// Generates "external" XML mapping for all the classes 
    /// </summary>
    /// <remarks>See http://iancooper.spaces.live.com/blog/cns!844BD2811F9ABE9C!397.entry and 
    /// http://msdn2.microsoft.com/en-us/library/bb386907.aspx for information on external mapping files.
    /// They are an alternative to attributing the individual classes.</remarks>
    public class MappingGenerator
    {
        public static MappingSource CreateMappingSource()
        {
            MappingGenerator generator = new MappingGenerator();
            string xml = generator.Xml;
            return XmlMappingSource.FromXml(xml);
        }

        public MappingGenerator()
        {
            StringBuilder result = new StringBuilder();
            result.Append(string.Format(_fileTemplate, GetProductName(), "DummyDBName")); //TODO better DB name
            result.AppendLine();
            foreach (Type t in GetAllEntityTypes())
            {
                result.Append(GenerateTable(t));
                result.AppendLine();
            }
            result.Append(_fileEnd);
            Xml = result.ToString();
        }

        public readonly string Xml;

        Dictionary<Assembly, object> _visitedAssemblies = new Dictionary<Assembly, object>();
        string _entityAssemblyName = typeof(IClose2PocoEntity).Assembly.FullName;

        string GetProductName()
        {
            var assembly = Assembly.GetExecutingAssembly();
            var attrib = (AssemblyProductAttribute)assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), true).Single();
            return attrib.Product;
        }

        /// <summary>
        /// Gets all classes which are persistent entities
        /// </summary>
        IEnumerable<Type> GetAllEntityTypes()
        {
            return from a in GetAllAssemblies()
                   where ReferencesEntityAssembly(a)  // saves time, and means that we avoid some errors which I got reflecting on low-level .NET framework types
                   from t in a.GetTypes()
                   where IsEntity(t)
                   select t;
        }


        IEnumerable<Assembly> GetAllAssemblies()
        {
            Assembly root = Assembly.GetEntryAssembly(); // in nunit testing, EntryAssembly was null
            if (root == null)
            {
                // find the first assembly, from all our callers, which is not THIS assembly
                // TODO: is this approach adequate?
                Assembly thisAssembly = Assembly.GetExecutingAssembly();
                StackTrace stack = new StackTrace();
                for (int i = 0; i < stack.FrameCount; i++ )
                {
                    StackFrame frame = stack.GetFrame(i);
                    Assembly callingAssembly = frame.GetMethod().DeclaringType.Assembly;
                    if (callingAssembly != thisAssembly)
                    {
                        root = callingAssembly;
                        break;
                    }
                }
            }
            if (root == null)
                throw new NotSupportedException("Could not find root assembly");

            return GetAssemblyTreeFrom(root);
        }

        /// <summary>
        /// Gets all assemblies referenced, directly or indirectly, from the given assembly.
        /// I.e. returns all assemblies in the tree of references, starting with the given root.
        /// Returns in a list structure, not an actual tree
        /// </summary>
        IEnumerable<Assembly> GetAssemblyTreeFrom(Assembly root)
        {
            // See: http://www.developersdex.com/asp/message.asp?p=2912&r=5817390
            // TODO: what about any assemblies to which there is not a design-time reference (e.g. dynamically loaded assemblies) ?

            if (_visitedAssemblies.ContainsKey(root))
                yield break;

            _visitedAssemblies.Add(root, null);
            yield return root;

            foreach(AssemblyName childName in root.GetReferencedAssemblies())
            {
                // did not use reflection-only version of load here because it complicates things by requiring pre-loading of referenced types, and it also places limitations on how attributes are used
                // TODO: is this fast enough? Or do we need to change to the reflection-only APIs, move this to before the root is yield (to get the pre-load) and be careful about how we work with attributes
                Assembly child;
                try
                {
                   child = Assembly.Load(childName.FullName);
                }
                catch
                {
                    //TODO log
                    yield break;
                }
                foreach (Assembly directOrIndirectChild in GetAssemblyTreeFrom(child))
                    yield return directOrIndirectChild;
            }
        }


        bool ReferencesEntityAssembly(Assembly a)
        {
            foreach (AssemblyName reference in a.GetReferencedAssemblies())
                if (reference.FullName == _entityAssemblyName)
                    return true;
            return false;
        }

        bool IsEntity(Type t)
        {
            return typeof(IClose2PocoEntity).IsAssignableFrom(t);
        }

        string GenerateTable(Type t)
        {
            StringBuilder result = new StringBuilder();
            foreach (PropertyInfo p in t.GetProperties().Where(p => IsPersistent(p)))
            {
                if (RelationshipFinder.IsEntityList(p))
                    result.AppendLine(GetListMapping(p));
                else if (RelationshipFinder.IsEntityReference(p))
                    result.AppendLine(GetReferenceMapping(p));
                else
                    result.AppendLine(GetStandardMapping(p));
            }
            string pluralEnding = string.Empty;  //TODO: drive this off a "pluralization" setting, to be "s" or String.Empty
            return string.Format(_tableTemplate, t.Name + pluralEnding, t.FullName, result);
        }

        /// <summary>
        /// Is the property persistent?  By default, all WRITABLE properties of entities are persistent, unless they have <see cref="TransientAttribute"/>
        /// Read-only properties are never persistent.
        /// </summary>
        bool IsPersistent(PropertyInfo p)
        {
            return p.CanWrite && p.GetCustomAttributes(typeof(TransientAttribute), true).Length == 0;
        }

        string GetListMapping(PropertyInfo p)
        {
            Relationship r = RelationshipFinder.GetRelationship(p);

            return string.Format(@"<Association Name=""TODO"" Member=""{0}"" Storage=""{1}"" OtherKey=""{2}"" {3} />",
                p.Name,
                "_" + p.Name.Substring(0, 1).ToLower() + p.Name.Substring(1),
                r.KeyAtRefEnd[0].Name, // Todo: handle multi part keys
                GetExtraAttributes(p));

        }

        string GetReferenceMapping(PropertyInfo p)
        {
            Relationship r = RelationshipFinder.GetRelationship(p);

            return string.Format(@"<Association Name=""TODO"" Member=""{0}"" Storage=""{1}"" ThisKey=""{2}"" {3}/>",
                p.Name,
                "_" + p.Name.Substring(0, 1).ToLower() + p.Name.Substring(1),
                r.KeyAtRefEnd[0].Name,   // TODO: handle multi-part keys
                GetExtraAttributes(p));
        }

        string GetStandardMapping(PropertyInfo p)
        {
            bool isInPrimaryKey = RelationshipFinder.GetPrimaryKeyFields(p.DeclaringType).Contains(p);
            string pkText = isInPrimaryKey ? @"IsPrimaryKey=""true""" : string.Empty;                       // this attribute is a special case, because sometimes we infer primary key from name
                                                                                                            // TODO: do we want to continue to do this?

            return string.Format(@"<Column Member=""{0}"" {1} {2}/>", p.Name, pkText, GetExtraAttributes(p));
        }

        string GetExtraAttributes(PropertyInfo p)  //TODO: add some validation of which attributes are valid on which kinds of properties (e.g. some may apply only to standard (not ref or list) properties)
        {
            StringBuilder result = new StringBuilder();
            var attributes = p.GetCustomAttributes(typeof (MappingAttribute), true);
            if (attributes.OfType<DbTypeAttribute>().Any() && attributes.OfType<LengthAttribute>().Any())
                throw new InvalidOperationException("Cannot mix " + typeof(DbTypeAttribute) + " and " + typeof(LengthAttribute) + " on property " + p.Name);
            foreach (MappingAttribute a in attributes)
            {
                result.Append(a.GetMappingText() + " ");
            }
            return result.ToString();
        }

        const string _fileTemplate =
@"<?xml version=""1.0"" encoding=""utf-8""?>
   <!-- This file is autogenerated by {0} 
       The XSD is downloaded from here:http://msdn2.microsoft.com/en-us/library/bb386907.aspx
       The attributes are described here: http://msdn2.microsoft.com/en-us/library/bb425822.aspx -->
<Database xmlns=""http://schemas.microsoft.com/linqtosql/mapping/2007""            
            xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" 
            xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" 
            Name=""{1}"">
";
        const string _fileEnd = "</Database>";

        const string _tableTemplate =
@"   <Table Name=""{0}"">
      <Type Name=""{1}"">
            {2}
        </Type>
    </Table>
    
";

    }
}
